--[[
for k, v in pairs(Spring.GetUnitPieceMap(unitID)) do
  Spring.Echo('piece ' .. k .. ' ' .. v)
end
--]]

SFX.CEG_X = SFX.CEG - 1
SFX.FIRE_WEAPON_X = SFX.FIRE_WEAPON - 1
SFX.DETONATE_WEAPON_X = SFX.DETONATE_WEAPON - 1

SIG_MOVE = 1
SIG_AIM = 2
SIG_AIM_X = SIG_AIM - 1
SIG_OPEN = 34
SIG_OPEN_X = SIG_OPEN - 1

FRAMES_PER_SECOND = Game.gameSpeed
FRAME_TIME = 1000 / Game.gameSpeed
ONE_FRAME  = 0.5 * FRAME_TIME
TWO_FRAMES = 1.5 * FRAME_TIME

local ud = UnitDefs[unitDefID]
local params = scriptParams
local min = math.min
local max = math.max
local abs = math.abs
local ceil = math.ceil
local rad = math.rad
local acos = math.acos
local random = math.random
local sqrt = math.sqrt
local PI = math.pi
local HALF_PI = math.pi / 2
local TWO_PI = math.pi * 2
local INFINITY = math.huge
local spGetUnitHealth = Spring.GetUnitHealth
local spGetUnitWeaponState = Spring.GetUnitWeaponState
local spSetUnitWeaponState = Spring.SetUnitWeaponState
local spGetPieceRotation = Spring.UnitScript.GetPieceRotation
local spGetHeadingFromVector = Spring.GetHeadingFromVector

local StopThreads = Signal
local SetThreadMask = SetSignalMask
SetSFXOccupy = setSFXoccupy  --standard case for function names

local HEADING_MULTIPLIER_RAD = math.pi / 32768

local function GetHeadingFromVectorRad (x, z)
  return (spGetHeadingFromVector(x, z) * HEADING_MULTIPLIER_RAD)
end

local function ClampRad (angle)
  angle = ((angle + PI) % TWO_PI) - PI
  return angle  --((angle >= 0) and angle or angle + TWO_PI)
end

local function ClampToFrame (timeLength)
  return (ceil(timeLength * FRAMES_PER_SECOND) / FRAMES_PER_SECOND)
end

local scriptState = {} 

local trackedPieces = {}


local function CheckPiece (piece, valueName)
  if (not piece) then
    Spring.Echo('["' .. ud.name .. '" UnitScript] Error: Piece ' .. valueName .. ' has nil value')
    return 1
  end
  return piece
end

local function CheckPieceArray (pieceArray, valueName)
  for i, piece in ipairs(pieceArray) do
    pieceArray[i] = CheckPiece (piece, valueName .. "["..i.."]")
  end
end

Spring.Echo("==========scripts.lua called1");

do
  --Spring.Echo("==========scripts.lua called2");
  params = params or {}
  params.smokeDelay = params.smokeDelay or 200
  params.maxRockAngleX = rad(params.maxRockAngleX or 0)
  params.maxRockAngleZ = rad(params.maxRockAngleZ or 0)
  if (type(params.weapons) ~= "table") then params.weapons = {} end
  local fixedWeapons = {}
  
  for i = 1, #(ud.weapons) do  --params.weapons
    fixedWeapons[i] = (type(params.weapons[i]) == "table") and params.weapons[i] or {}
    local weapon = fixedWeapons[i]

    weapon.id              = i
    weapon.unitDef         = ud.weapons[i]
    weapon.def             = WeaponDefs[weapon.unitDef.weaponDef]
    weapon.turretTurnSpeed = rad(weapon.turretTurnSpeed or 90)
    weapon.sleeveTurnSpeed = rad(weapon.sleeveTurnSpeed or 90)
    weapon.barrelRecoil    = weapon.barrelRecoil or 0
    weapon.projectileSpeed = spGetUnitWeaponState (unitID, i - 1, "projectileSpeed") * Game.gameSpeed  --weapon.def.startvelocity
    weapon.rockForce       = weapon.rockForce or 0
    weapon.tolerance       = rad(weapon.def.maxAngle)

    ----------
    weapon.maxHeadingDif   = weapon.maxHeadingDif and rad(weapon.maxHeadingDif) or PI
    weapon.maxPitchDif     = weapon.maxPitchDif and rad(weapon.maxPitchDif) or PI
    weapon.minPitch        = weapon.minPitch and rad(weapon.minPitch) or -HALF_PI
    local mainDirY = weapon.unitDef.mainDirY
    weapon.mainDirPitch    = GetHeadingFromVectorRad (mainDirY, sqrt(1 - mainDirY * mainDirY))	

    if (weapon.unitDef.maxAngleDif ~= -1) then
      local maxAngleDif = acos(weapon.unitDef.maxAngleDif)	
      --if (weapon.maxHeadingDif >= maxAngleDif) then weapon.maxHeadingDif = PI end  --still needed for turning
      if (weapon.maxPitchDif >= maxAngleDif) then weapon.maxPitchDif = PI end
      if (weapon.minPitch <= weapon.mainDirPitch - maxAngleDif) then weapon.maxPitchDif = -HALF_PI end
    end

    if (weapon.maxHeadingDif < PI) then
      weapon.mainDirHeading        = GetHeadingFromVectorRad (weapon.unitDef.mainDirZ, weapon.unitDef.mainDirX)	
      weapon.reverseMainDirHeading = ClampRad (weapon.mainDirHeading + PI)
      weapon.minHeading            = ClampRad (weapon.mainDirHeading - weapon.maxHeadingDif)
      weapon.maxHeading            = ClampRad (weapon.mainDirHeading + weapon.maxHeadingDif)
    else
      weapon.maxHeadingDif = nil
    end  
    weapon.minPitch        = max(-HALF_PI, weapon.mainDirPitch - weapon.maxPitchDif, weapon.minPitch)
    weapon.maxPitch        = min( HALF_PI, weapon.mainDirPitch + weapon.maxPitchDif)
    weapon.maxPitchDif     = nil
    ----------
    
    if (type(weapon.barrel   ) ~= "table") then weapon.barrel    = { weapon.barrel } end
    if (type(weapon.firepoint) ~= "table") then weapon.firepoint = { weapon.firepoint or false } end
    weapon.turret = CheckPiece(weapon.turret, "weapons["..i.."].turret")
    CheckPiece(weapon.sleeve, "weapons["..i.."].sleeve")
    CheckPieceArray(weapon.barrel, "weapons["..i.."].barrel")
    CheckPieceArray(weapon.firepoint, "weapons["..i.."].firepoint")
  
    weapon.numBarrels      = #weapon.barrel
    weapon.numFirePoints   = #weapon.firepoint
    
    weapon.usedBarrelID = 1
    --weapon.usedBarrel = weapon.barrel[1]
    weapon.usedFirePointID = 1
    weapon.usedFirePoint = weapon.firepoint[1]
    weapon.isAiming = false
    weapon.isFiring = false
  
    if (weapon.rotatesWith) then
      local piece = weapon.rotatesWith
      if (not trackedPieces[piece]) then
        trackedPieces[piece] = {
          dependencies = {},
          rotationSpeed = 0,
          rotationTarget = 0,
        }
      end
      table.insert(trackedPieces[piece].dependencies, weapon)
      weapon.trackingData = trackedPieces[piece]
    end
  end
  params.weapons = fixedWeapons
  
  --VFS.Include("luarules/utils/debug.lua")
  --printTable ("scriptParams", params)

  GG.ScriptGlobal = GG.ScriptGlobal or {}
  GG.ScriptGlobal.ScriptDefs = GG.ScriptGlobal.ScriptDefs or {}
  GG.ScriptGlobal.ScriptDefs[unitDefID] = GG.ScriptGlobal.ScriptDefs[unitDefID] or {}
  if (next(params.weapons)) then
    local sd = GG.ScriptGlobal.ScriptDefs[unitDefID]
    sd.weapons = sd.weapons or {}
    for i, w in ipairs(params.weapons) do  --params.weapons
      --sd.weapons[i] = { turretPiece = w.turret }
    end  
  end
end


function script.Create()
  for _, weapon in ipairs(params.weapons) do
    for i, firePoint in ipairs(weapon.firepoint) do
      if (firePoint ~= weapon.barrel[i]) then
        Hide (firePoint)
      end
    end
  
    Turn (weapon.sleeve, x_axis, weapon.mainDirPitch)	
  end	

  StartThread (SmokeUnit)
end


function script.StartMoving()
  StopThreads (SIG_MOVE)
  SetThreadMask (SIG_MOVE)
 
  local enginePieces, smokeDelay = params.enginePieces, params.smokeDelay
  if (enginePieces and enginePieces[1]) then
    local n = #enginePieces
  
    while true do
      local health, maxHealth = spGetUnitHealth(unitID)
      local healthPercent = health / maxHealth	
    
      for i = 1,n do
        local smokeType = (healthPercent > .66 * random()) and SFX.WHITE_SMOKE or SFX.BLACK_SMOKE	  
        EmitSfx (enginePieces[i], smokeType)
      end	
      Sleep (params.smokeDelay)
    end
  end	
end


function script.StopMoving()
  StopThreads (SIG_MOVE)
end


function SmokeUnit()
  if not (smokePieces and smokePieces[1]) then return end
  local n = #smokePieces

  while true do
    local health, maxHealth, _,_,buildPercent = spGetUnitHealth(unitID)
  
    if (buildPercent < 100) then
      Sleep(400)  --1000
    else   
      local healthPercent = health / maxHealth
      if (healthPercent < .66) then  --only smoke if less then 2/3rd health left
        local smokeType = (healthPercent > .66 * random()) and SFX.WHITE_SMOKE or SFX.BLACK_SMOKE
        EmitSfx (smokePieces[ random(1, n) ], smokeType)
      end
      local sleepTime = random(100, 200) + 100 * healthPercent  --200 + 100 * healthPercent  --50 * healthPercent 
      Sleep(sleepTime)
    end  
  end
end


function Open (weaponNum) 
end


function Close (weaponNum) 
end


local function RestoreAfterDelay (weaponNum)
  local weapon = params.weapons[weaponNum]  
  local restoreDelay = weapon.restoreDelay or (1000 * 3 * max(1.0, weapon.def.reload))
  --spGetUnitWeaponState(unitID, weaponNum - 1, "reloadTime") FIXME

  --[[local ID = random(1000)
  Spring.Echo("RestoreAfterDelay("..weaponNum..") ID: "..ID.." started")
  for i = 1, (restoreDelay / FRAME_TIME) do
    Spring.Echo("RestoreAfterDelay("..weaponNum..") ID: "..ID.." running...")
  Sleep (ONE_FRAME)
  end--]]
  Sleep (restoreDelay)
  Turn (weapon.turret, y_axis, 0, 0.5 * weapon.turretTurnSpeed)
  Turn (weapon.sleeve, x_axis, weapon.mainDirPitch, 0.5 * weapon.sleeveTurnSpeed)
  
  if (weapon.openAnimation) then
    WaitForTurn (weapon.turret, y_axis)
    WaitForTurn (weapon.sleeve, x_axis)
    Close (weaponNum) 
  end
end


local function TurnInSteps (piece, axis, angle, speed)
  local _,pieceHeading = spGetPieceRotation (piece)
  local absAngle = abs(angle)
  if (absAngle >= PI) then
    local stepSize = (ClampToFrame (PI / speed) - FRAME_TIME) * speed
    if (angle < 0) then stepSize = -stepSize end
    local angleCompleted = stepSize
    repeat
      Turn (piece, axis, pieceHeading + angleCompleted, speed)
      WaitForTurn (piece, axis)
      angleCompleted = angleCompleted + stepSize
    until (abs(angleCompleted) >= absAngle) 
  end
  Turn (piece, axis, pieceHeading + angle, speed)    
end


function script.AimWeapon (weaponNum, heading, pitch)
  --Spring.Echo("AimWeapon: "..weaponNum.." "..math.deg(heading).." "..math.deg(pitch))
  StopThreads (SIG_AIM_X + weaponNum)    -- Kill all aim threads for this unit
  SetThreadMask (SIG_AIM_X + weaponNum)  -- Mark this thread as an aim thread,
                                         -- so a subsequent Signal(SIG_AIM) call kills it.
  local isAimed = false
  local weapon = params.weapons[weaponNum]
  weapon.isAiming = true
  local _,myHeading = spGetPieceRotation (weapon.turret)
  local wantedPitch = min(max(weapon.minPitch, pitch), weapon.maxPitch)
  
  Turn (weapon.sleeve, x_axis, -wantedPitch, weapon.sleeveTurnSpeed)
  if (weapon.rotatesWith) then
    weapon.lastHeading = heading
    weapon.lastPitch = pitch
  
    local _,trackingPieceHeading = spGetPieceRotation (weapon.rotatesWith)  --FIXME: check recursively
    local wantedHeading
    wantedHeading = heading - trackingPieceHeading
    --Spring.Echo("myHeading: "..myHeading.." wantedHeading: "..math.deg(wantedHeading))
    if (weapon.trackingData.rotationSpeed == 0.0) then
      wantedHeading = heading - trackingPieceHeading
      weapon.predictedAimingTime = 0
    else
      --brain f***ing logic, don't try to understand
      local plusHeadingDif = ((heading - trackingPieceHeading) - myHeading) % TWO_PI
      local minusHeadingDif = plusHeadingDif - TWO_PI
      local plusTurnSpeed = weapon.turretTurnSpeed + weapon.trackingData.rotationSpeed
      local minusTurnSpeed = -weapon.turretTurnSpeed + weapon.trackingData.rotationSpeed	  
      local plusPredictedTime = (plusTurnSpeed == 0) and INFINITY or (((plusTurnSpeed >= 0) and plusHeadingDif or minusHeadingDif) / plusTurnSpeed)
      local minusPredictedTime = (minusTurnSpeed == 0) and INFINITY or (((minusTurnSpeed >= 0) and plusHeadingDif or minusHeadingDif) / minusTurnSpeed)
      local predictedTime = ClampToFrame (min(plusPredictedTime, minusPredictedTime))
      local predictedTPHeading = trackingPieceHeading + (weapon.trackingData.rotationSpeed * predictedTime)
      wantedHeading = heading - predictedTPHeading
      weapon.predictedAimingTime = predictedTime
      --[[Spring.Echo("heading: "..math.deg(heading).." myHeading: "..math.deg(myHeading).." trackingPieceHeading: "..math.deg(trackingPieceHeading))
      Spring.Echo("plusHeadingDif: "..math.deg(plusHeadingDif).." minusHeadingDif: "..math.deg(minusHeadingDif))
      Spring.Echo("plusTurnSpeed: "..math.deg(plusTurnSpeed).." minusTurnSpeed: "..math.deg(minusTurnSpeed))
      Spring.Echo("plusPredictedTime: "..plusPredictedTime.." minusPredictedTime: "..minusPredictedTime)
      Spring.Echo("predictedTime: "..predictedTime.." predictedTPHeading: "..math.deg(predictedTPHeading))--]]	  
      --[[local predictedPlusHeadingDif = ((heading - predictedTPHeading) - myHeading) % TWO_PI
      if (plusPredictedTime > minusPredictedTime) then
        predictedPlusHeadingDif = predictedPlusHeadingDif - TWO_PI
      end--]]
    end

    --if (headingDif < 0.001) then break end
    Turn (weapon.turret, y_axis, wantedHeading, weapon.turretTurnSpeed)
    WaitForTurn (weapon.turret, y_axis)
    weapon.predictedAimingTime = 0
    _,trackingPieceHeading = spGetPieceRotation (weapon.rotatesWith)
    --Spring.Echo("Finished turn. TPHeading "..math.deg(trackingPieceHeading))
    
    if (weapon.trackingData.rotationSpeed ~= 0.0) then
      if (not Spring.UnitScript.IsInTurn (weapon.sleeve, x_axis)) then
        spSetUnitWeaponState (unitID, weaponNum - 1, "aimReady", 1)
      end  
    
      _,trackingPieceHeading = spGetPieceRotation (weapon.rotatesWith)
      local rotationLeft = ClampRad (weapon.trackingData.rotationTarget - trackingPieceHeading)  --FIX_REVERSE
      Turn (weapon.turret, y_axis, wantedHeading - rotationLeft, abs(weapon.trackingData.rotationSpeed))
    end  
  else
    local wantedHeading = heading
    local reverseTurning = false
    
    if (wantedHeading ~= myHeading and weapon.maxHeadingDif) then
      local mainHeadingDif = ClampRad (wantedHeading - weapon.mainDirHeading)
      if (mainHeadingDif < -weapon.maxHeadingDif) then
        wantedHeading = weapon.minHeading
      elseif (mainHeadingDif > -weapon.maxHeadingDif) then
        wantedHeading = weapon.maxHeading
      end
      if (wantedHeading ~= myHeading) then
        local headingDif = ClampRad (wantedHeading - myHeading)
        local reverseHeadingDif = ClampRad (weapon.reverseMainDirHeading - myHeading)
        if ((headingDif < 0) == (reverseHeadingDif < 0) and abs(headingDif) > abs(reverseHeadingDif)) then
          reverseTurning = true 
        end
      end  
    end  
    
    if (wantedHeading ~= myHeading) then    
      local tracked = trackedPieces[weapon.turret]
      if (tracked) then
        --Spring.Echo("Old rotations: "..tracked.rotationSpeed.." "..tracked.rotationTarget)
        local headingDif = ClampRad (wantedHeading - myHeading)
        local oldRotationSpeed = tracked.rotationSpeed
        local oldRotationTarget = tracked.rotationTarget
        tracked.rotationSpeed = (headingDif >= 0) and weapon.turretTurnSpeed or -weapon.turretTurnSpeed
        if (reverseTurning) then tracked.rotationSpeed = -tracked.rotationSpeed end
        --Spring.Echo("PieceRotation: "..myHeading.." New heading: "..wantedHeading.." rotationSpeed: "..tracked.rotationSpeed)
        tracked.rotationTarget = wantedHeading
        --Spring.Echo("New rotations: "..tracked.rotationSpeed.." "..tracked.rotationTarget)
        
        if (tracked.rotationSpeed ~= oldRotationSpeed or tracked.rotationTarget ~= oldRotationTarget) then
          local predictedTime = -1
          if (tracked.rotationSpeed == oldRotationSpeed) then
            local absHeadingDif = reverseTurning and (TWO_PI - abs(headingDif)) or abs(headingDif)
            predictedTime = ClampToFrame (absHeadingDif / weapon.turretTurnSpeed)
          end  
        
          for _, dep in ipairs(tracked.dependencies) do
            if ((dep.isAiming or dep.isFiring) and predictedTime < dep.predictedAimingTime) then
              --Spring.Echo("Calling AimWeapon: "..dep.id) 
               script.AimWeapon (dep.id, dep.lastHeading, dep.lastPitch)  
            end
          end
        end
      end 
      
      if (reverseTurning) then
        TurnInSteps (weapon.turret, y_axis, angle, weapon.turretTurnSpeed)
        --Turn (, , weapon.mainDirHeading, ) TODO
      else
        Turn (weapon.turret, y_axis, wantedHeading, weapon.turretTurnSpeed)  --FIXME: fast turning bug
      end
      WaitForTurn (weapon.turret, y_axis)
    end     
    
    if (tracked and tracked.rotationSpeed ~= 0) then
      tracked.rotationSpeed = 0
      
      for _, dep in ipairs(tracked.dependencies) do
        if (dep.isAiming or dep.isFiring) then
          --Spring.Echo("Calling AimWeapon: "..dep.id) 		
          script.AimWeapon (dep.id, dep.lastHeading, dep.lastPitch)
        end
      end	  
    end 	
  end	
  WaitForTurn (weapon.sleeve, x_axis)
  isAimed = true
  weapon.isAiming = false
  
  if (weapon.restores) then
    StartThread (RestoreAfterDelay, weaponNum)
  end	
  return isAimed
end


local function NextFirePoint (weaponNum)
  local weapon = params.weapons[weaponNum]
  if (weapon.usedFirePointID < weapon.numFirePoints) then
    weapon.usedBarrelID = ((weapon.usedBarrelID < weapon.numBarrels) and weapon.usedBarrelID + 1 or 1)
    weapon.usedFirePointID = weapon.usedFirePointID + 1
  else
    weapon.usedBarrelID = 1
    weapon.usedFirePointID = 1
  end
  weapon.usedFirePoint = weapon.firepoint[weapon.usedFirePointID]
  --weapon.firepoint[usedBarrel] or weapon.firepoint[1]  
end


function script.AimFromWeapon (weaponNum) 
  return params.weapons[weaponNum].turret
end


function script.QueryWeapon (weaponNum)
  return params.weapons[weaponNum].usedFirePoint
end


function script.FireWeapon (weaponNum)
  --Spring.Echo("FireWeapon: "..weaponNum)
  params.weapons[weaponNum].isFiring = true
end


function script.Shot (weaponNum)
  local weapon = params.weapons[weaponNum]
  --Spring.Echo("Shot: "..weaponNum--[[.." "..weapon.usedBarrelID.." "..weapon.usedFirePointID--]])  
  local barrel 
  
  if (weapon.barrelRecoil ~= 0) then
    barrel = weapon.barrel[weapon.usedBarrelID]
    if (barrel) then 
      Move (barrel, z_axis, -weapon.barrelRecoil, weapon.projectileSpeed)
    end
  end	
  if (weapon.cegID) then 
    EmitSfx (weapon.usedFirePoint, SFX.CEG_X + weapon.cegID)
  end	
  if (barrel) then
    Sleep (150)
    --WaitForMove (barrel, z_axis)
    Move (barrel, z_axis, 0, 3)
  end

  if (weapon.numFirePoints > 1) then
    NextFirePoint(weaponNum)
  end   
end

function script.EndBurst (weaponNum)
  --Spring.Echo("EndBurst: "..weaponNum)
  params.weapons[weaponNum].isFiring = false  
end 

function script.BlockShot (weaponNum, targetUnitID, userTarget)  --FIXME: doesn't work
  --Spring.Echo("BlockShot: "..weaponNum)
  local weapon = params.weapons[weaponNum]  
  return (weapon.isAiming)
end

function script.TargetWeight (weaponNum, targetUnitID)  --doesn't work
  --Spring.Echo("TargetWeight: "..weaponNum)
  return 1.0
end  


function script.RockUnit (x, z)
  if (not body) then return end
  --Spring.Echo("RockUnit: "..--[[x.." "..z.." "..--]]math.deg(GetHeadingFromVectorRad(x, z)) + 180)
  --[[Turn (body, x_axis, z * params.maxRockAngleZ, rad(50))
  Turn (body, z_axis, -x * params.maxRockAngleX, rad(50))
  WaitForTurn (body, z_axis)
  WaitForTurn (body, x_axis)
  Turn (body, z_axis, 0, rad(20))
  Turn (body, x_axis, 0, rad(20))--]]
end


function script.HitByWeapon (x, z, weaponDefID, damage)  --HitByWeaponId
  if (not body or (params.maxRockAngleX <= 0 and params.maxRockAngleZ)) then return end
  --Spring.Echo("HitByWeapon: "..x.." "..z.." "..weaponDefID.." "..damage)
  Turn (body, x_axis, z * params.maxRockAngleZ, rad(105))
  Turn (body, z_axis, -x * params.maxRockAngleX, rad(105))
  WaitForTurn (body, x_axis)
  WaitForTurn (body, z_axis)
  Turn (body, x_axis, 0, rad(30))
  Turn (body, z_axis, 0, rad(30))
  return damage
end